/*            Copyright(c) 2006-2008 Dave Sexton           *
 * http://www.codeplex.com/DocProject                      *
 * http://www.codeplex.com/DocProject/Project/License.aspx *
 ***********************************************************/
using System;
using System.Collections.Generic;
using System.Text;
using DaveSexton.DocProject.Engine;
using System.Threading;
using DaveSexton.DocProject.Resources;

namespace DaveSexton.DocProject
{
	public enum BuildState
	{
		Running,
		Verifying,
		Successful,
		Failed
	}

	/// <remarks>
	/// <para>
	/// This class may be accessed by threads other than the UI thread, although it's not thread-safe.  Do not cache instances of this class
	/// or call into it asynchronously.
	/// </para>
	/// <para>
	/// Note: <see cref="IBuildStep" /> implementations and <see cref="BuildProcessComponent" />s are executed
	/// synchronously, even when they run in the background.  Therefore, they can call into the class as long as the instance isn't used 
	/// asynchronously.
	/// </para>
	/// <para>
	/// This class is designed to cache values associated with automation components so that the components themselves are not accessed, 
	/// inadvertantly, by background threads.
	/// </para>
	/// </remarks>
	public sealed class BuildContext : MarshalByRefObject
	{
		#region Public Properties
		public IBuildEngine Engine
		{
			get
			{
				return engine;
			}
		}

		public BuildStepCollection Steps
		{
			get
			{
				// The engine's BuildStarting event provides the BuildContext as an event argument property.  In order to
				// ensure that event handlers have access to the engine's steps collection it's returned instead of the 
				// local, empty collection.  After the CopyStepsFromEngine method is called, the local, initialized instance
				// is returned for the remainder of the lifetime of the BuildContext object.  This collection may have a 
				// reduced number of steps due to the FilterStepsForPartialBuild method being called.
				return (!copiedSteps) ? engine.Steps : steps;
			}
		}

		public BuildStepCollection FailedSteps
		{
			get
			{
				return failedSteps;
			}
		}

		public BuildStepCollection SuccessfulSteps
		{
			get
			{
				return successfulSteps;
			}
		}

		public BuildStepCollection SkippedSteps
		{
			get
			{
				return skippedSteps;
			}
		}

		public int CurrentStepIndex
		{
			get
			{
				return currentStepIndex;
			}
		}

		public IBuildStep CurrentStep
		{
			get
			{
				return (currentStepIndex == -1) ? null : Steps[currentStepIndex];
			}
		}

		public string CurrentDirectory
		{
			get
			{
				return currentDirectory;
			}
			set
			{
				if (string.IsNullOrEmpty(value))
					throw new ArgumentException(Errors.NullOrEmpty, "value");

				currentDirectory = value;
			}
		}

		public string ProjectDirectory
		{
			get
			{
				return projectDirectory;
			}
		}

		public int SkippedStepsCount
		{
			get
			{
				return skippedSteps.Count;
			}
		}

		public int SkippedOptionalStepsCount
		{
			get
			{
				int count = 0;

				foreach (IBuildStep step in skippedSteps)
					if (step.ContinueOnError)
						count++;

				return count;
			}
		}

		public int SkippedRequiredStepsCount
		{
			get
			{
				int count = 0;

				foreach (IBuildStep step in skippedSteps)
					if (!step.ContinueOnError)
						count++;

				return count;
			}
		}

		public int FailedStepsCount
		{
			get
			{
				return failedSteps.Count;
			}
		}

		public int FailedOptionalStepsCount
		{
			get
			{
				int count = 0;

				foreach (IBuildStep step in failedSteps)
					if (step.ContinueOnError)
						count++;

				return count;
			}
		}

		public int FailedRequiredStepsCount
		{
			get
			{
				int count = 0;

				foreach (IBuildStep step in failedSteps)
					if (!step.ContinueOnError)
						count++;

				return count;
			}
		}

		public int SuccessfulStepsCount
		{
			get
			{
				return successfulSteps.Count;
			}
		}

		public int ExecutedStepsCount
		{
			get
			{
				return failedSteps.Count + successfulSteps.Count;
			}
		}

		public bool SuccessfullyExecutedAllRequiredSteps
		{
			get
			{
				foreach (IBuildStep step in Steps)
				{
					if (!successfulSteps.Contains(step) && 
						((!step.ContinueOnError) || (!skippedSteps.Contains(step) && !failedSteps.Contains(step))))
						return false;
				}

				return true;
			}
		}

		public int PercentComplete
		{
			get
			{
				return (currentStepIndex == -1) ? 100 : (currentStepIndex + 1) * 100 / Steps.Count;		// use Steps property, not field
			}
		}

		public SynchronizationContext Sync
		{
			get
			{
				return sync;
			}
			set
			{
				if (value == null)
					throw new ArgumentNullException("value");

				sync = value;
			}
		}

		public BuildState BuildState
		{
			get
			{
				return buildState;
			}
			internal set
			{
				buildState = value;
			}
		}

		public string ProjectName
		{
			get
			{
				return projectName;
			}
		}

		public bool Canceled
		{
			get
			{
				return cancel;
			}
		}

		public BuildType BuildType
		{
			get
			{
				return buildType;
			}
			internal set
			{
				if (!Enum.IsDefined(typeof(BuildType), buildType))
					throw new System.ComponentModel.InvalidEnumArgumentException("value", (int) buildType, typeof(BuildType));

				buildType = value;
			}
		}

		public bool IsDocSite
		{
			get
			{
				return isDocSite;
			}
		}

		public int ItemErrorCount
		{
			get
			{
				return itemErrorCount;
			}
		}

		public SourceCollection Sources
		{
			get
			{
				return sources;
			}
			internal set
			{
				sources = value;
			}
		}

		public DocProjectItemCollection BuildItems
		{
			get
			{
				return buildItems;
			}
			internal set
			{
				buildItems = value;
			}
		}

		public System.Threading.EventWaitHandle WaitHandle
		{
			get
			{
				return waitHandle;
			}
		}
		#endregion

		#region Private / Protected
		private BuildType buildType = BuildType.Default;
		private BuildState buildState;
		private int currentStepIndex, itemErrorCount;
		private string currentDirectory;
		private readonly IBuildEngine engine;
		private readonly BuildStepCollection failedSteps, successfulSteps, skippedSteps, steps;
		private SynchronizationContext sync;
		private readonly string projectName, projectDirectory;
		private readonly bool isDocSite;
		private volatile bool cancel;
		private bool copiedSteps;
		private readonly BuildTrace trace;
		private SourceCollection sources;
		private DocProjectItemCollection buildItems;
		private readonly System.Threading.EventWaitHandle waitHandle = new EventWaitHandle(true, EventResetMode.ManualReset);

		internal BuildStepExecutionEngine stepEngine;
		internal BuildProcessComponentProxy processComponent;
		#endregion

		#region Constructors
		/// <summary>
		/// Constructs a new instance of the <see cref="BuildContext" /> class.
		/// </summary>
		public BuildContext(IBuildEngine engine, BuildTrace trace)
		{
			if (engine == null)
				throw new ArgumentNullException("engine");

			if (trace == null)
				throw new ArgumentNullException("trace");

			this.engine = engine;
			this.trace = trace;

			skippedSteps = new BuildStepCollection(engine);
			failedSteps = new BuildStepCollection(engine);
			successfulSteps = new BuildStepCollection(engine);
			sync = SynchronizationContext.Current ?? new SynchronizationContext();

			isDocSite = engine.Project.IsDocSite;
			projectName = engine.Project.Name;
			projectDirectory = engine.Project.Directory;

			// Creating the collection does not automatically reference the engine's steps - they still must be added to the collection.
			// The actual steps are copied by the CopyStepsFromEngine method, called after the engine's BuildStarting event has been raised.
			// This gives plug-ins the chance to add more steps to a derived or composited engine before the build starts.
			// (The copy operation must occur before the FilterStepsForPartialBuild method is called)
			steps = new BuildStepCollection(engine);
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="BuildContext" /> class for the specified <paramref name="steps" />.
		/// </summary>
		public BuildContext(IBuildEngine engine, BuildTrace trace, BuildStepCollection steps)
			: this(engine, trace)
		{
			if (steps == null)
				throw new ArgumentNullException("steps");

			foreach (IBuildStep step in steps)
				this.steps.Add(step);

			copiedSteps = true;
		}
		#endregion

		#region Methods
		[System.Security.Permissions.SecurityPermission(System.Security.Permissions.SecurityAction.LinkDemand, 
			Flags = System.Security.Permissions.SecurityPermissionFlag.Infrastructure)]
		public override object InitializeLifetimeService()
		{
			return null;
		}

		public void InvokeAsync(Callback target)
		{
			InvokeInternal(true, false, sync, target, null);
		}

		public void InvokeAsync(Delegate target, params object[] arguments)
		{
			InvokeInternal(true, false, sync, target, arguments);
		}

		/// <remarks>
		/// The specified <paramref name="target"/> will be invoked asynchronously if it doesn't return a value and if the 
		/// call is coming from the UI thread's <see cref="System.Threading.SynchronizationContext"/>.  A synchronous invocation
		/// will occur in all other scenarios.
		/// <para>To force asynchronous invocation, use one of the <see cref="InvokeAsync"/> methods instead.</para>
		/// </remarks>
		public void Invoke(Callback target)
		{
			// Since this Invoke overload has a return value, the code must use a syncronous send.
			InvokeInternal(false, cancel, sync, target, null);
		}

		/// <remarks>
		/// The specified <paramref name="target"/> will be invoked asynchronously if it doesn't return a value and if the 
		/// call is coming from the UI thread's <see cref="System.Threading.SynchronizationContext"/>.  A synchronous invocation
		/// will occur in all other scenarios.
		/// <para>To force asynchronous invocation, use one of the <see cref="InvokeAsync"/> methods instead.</para>
		/// </remarks>
		public object Invoke(Delegate target, params object[] arguments)
		{
			// Since this Invoke overload has a return value, the code must use a syncronous send.
			return InvokeInternal(false, cancel, sync, target, arguments);
		}

		private static object InvokeInternal(bool forceAsync, bool canceled, SynchronizationContext context, Delegate target, params object[] arguments)
		{
			if (target == null)
				throw new ArgumentNullException("target");

			object returnValue = null;

			if (!forceAsync && context == System.Threading.SynchronizationContext.Current)
				returnValue = target.DynamicInvoke(arguments);
			else
			{
				SendOrPostCallback callback = delegate(object state)
				{
					returnValue = target.DynamicInvoke(arguments);
				};

				if (forceAsync || target.Method.ReturnType == typeof(void))
					context.Post(callback, null);
				else if (canceled)
					// Since the send is synchronous, invocation must not occur if the context is canceled to avoid deadlocks on the UI thread (occurred in testing).
					throw new OperationCanceledException();
				else
					context.Send(callback, null);
			}

			return returnValue;
		}

		internal void AddFailedStep(IBuildStep step)
		{
			if (step.Engine != engine)
				throw new ArgumentException(Errors.BuildStepDifferentEngine, "step");

			failedSteps.Add(step);

			if (currentStepIndex == Steps.Count - 1)
				currentStepIndex = -1;
			else
				currentStepIndex++;
		}

		internal void AddSuccessfulStep(IBuildStep step)
		{
			if (step.Engine != engine)
				throw new ArgumentException(Errors.BuildStepDifferentEngine, "step");

			successfulSteps.Add(step);

			if (currentStepIndex == Steps.Count - 1)
				currentStepIndex = -1;
			else
				currentStepIndex++;
		}

		internal void SkippedStep(IBuildStep step)
		{
			if (step.Engine != engine)
				throw new ArgumentException(Errors.BuildStepDifferentEngine, "step");

			skippedSteps.Add(step);

			if (currentStepIndex == Steps.Count - 1)
				currentStepIndex = -1;
			else
				currentStepIndex++;
		}

		public void Cancel()
		{
			if (cancel)
				return;

			cancel = true;

			sync.Post(delegate(object state)
			{
				OnCanceledChanged(EventArgs.Empty);
			}, null);
		}

		internal void CopyStepsFromEngine()
		{
			foreach (IBuildStep step in engine.Steps)
				steps.Add(step);

			copiedSteps = true;
		}

		internal void FilterStepsForPartialBuild()
		{
			if (buildType == BuildType.Partial)
			{
				foreach (IBuildStep step in engine.Steps)
					if (!step.IncludeInPartialBuild)
						steps.Remove(step);
			}
		}

		public bool ExecuteOutOfBand(params IBuildStep[] buildSteps)
		{
			return stepEngine.ExecuteSequence(buildSteps);
		}

		public bool ExecuteOutOfBand(BuildStepCollection buildSteps)
		{
			return stepEngine.ExecuteSequence(new List<IBuildStep>(buildSteps).ToArray());
		}

		#region Build Output
		public void Trace(string message)
		{
			Invoke(delegate()
			{
				trace.Trace(message);
			});
		}

		public void Trace(string format, params object[] args)
		{
			Invoke(delegate()
			{
				trace.Trace((args == null)
					? format
					: string.Format(System.Globalization.CultureInfo.CurrentCulture, format, args));
			});
		}

		public void TracePath(string path)
		{
			Invoke(delegate()
			{
				trace.Trace(Environment.ExpandEnvironmentVariables(path));
			});
		}

		public void TracePathLine(string path)
		{
			Invoke(delegate()
			{
				trace.TraceLine(Environment.ExpandEnvironmentVariables(path));
			});
		}

		public void TraceLine()
		{
			Invoke(delegate()
			{
				trace.TraceLine();
			});
		}

		public void TraceLine(string message)
		{
			Invoke(delegate()
			{
				trace.TraceLine(message);
			});
		}

		public void TraceLine(string format, params object[] args)
		{
			Invoke(delegate()
			{
				trace.TraceLine((args == null)
					? format
					: string.Format(System.Globalization.CultureInfo.CurrentCulture, format, args));
			});
		}

		public void SetProgress(string text, int completed, int total)
		{
			Invoke(delegate()
			{
				trace.SetProgress(text, completed, total);
			});
		}

		public void SetStatus(string text)
		{
			Invoke(delegate()
			{
				trace.SetStatus(text);
			});
		}
		#endregion

		#region Task List
		public void AddInfoToList(string message)
		{
			AddInfoToList(null, message, string.Empty, 0, false);
		}

		public void AddWarningToList(string message)
		{
			AddWarningToList(null, message, string.Empty, 0, false);
		}

		public void AddErrorToList(string message)
		{
			AddErrorToList(null, message, string.Empty, 0, false);
		}

		public void AddInfoToList(string message, bool showInBuildOutput)
		{
			AddInfoToList(null, message, string.Empty, 0, showInBuildOutput);
		}

		public void AddWarningToList(string message, bool showInBuildOutput)
		{
			AddWarningToList(null, message, string.Empty, 0, showInBuildOutput);
		}

		public void AddErrorToList(string message, bool showInBuildOutput)
		{
			AddErrorToList(null, message, string.Empty, 0, showInBuildOutput);
		}

		public void AddInfoToList(string category, string message)
		{
			AddInfoToList(category, message, string.Empty, 0, false);
		}

		public void AddWarningToList(string category, string message)
		{
			AddWarningToList(category, message, string.Empty, 0, false);
		}

		public void AddErrorToList(string category, string message)
		{
			AddErrorToList(category, message, string.Empty, 0, false);
		}

		public void AddInfoToList(string category, string message, string buildOutputMessage)
		{
			AddInfoToList(category, message, string.Empty, 0, buildOutputMessage);
		}

		public void AddWarningToList(string category, string message, string buildOutputMessage)
		{
			AddWarningToList(category, message, string.Empty, 0, buildOutputMessage);
		}

		public void AddErrorToList(string category, string message, string buildOutputMessage)
		{
			AddErrorToList(category, message, string.Empty, 0, buildOutputMessage);
		}

		public void AddInfoToList(string category, string message, bool showInBuildOutput)
		{
			AddInfoToList(category, message, string.Empty, 0, showInBuildOutput);
		}

		public void AddWarningToList(string category, string message, bool showInBuildOutput)
		{
			AddWarningToList(category, message, string.Empty, 0, showInBuildOutput);
		}

		public void AddErrorToList(string category, string message, bool showInBuildOutput)
		{
			AddErrorToList(category, message, string.Empty, 0, showInBuildOutput);
		}

		public void AddInfoToList(string message, string file, int line)
		{
			AddInfoToList(null, message, file, line, false);
		}

		public void AddWarningToList(string message, string file, int line)
		{
			AddWarningToList(null, message, file, line, false);
		}

		public void AddErrorToList(string message, string file, int line)
		{
			AddErrorToList(null, message, file, line, false);
		}

		public void AddInfoToList(string message, string file, int line, bool showInBuildOutput)
		{
			AddInfoToList(null, message, file, line, showInBuildOutput);
		}

		public void AddWarningToList(string message, string file, int line, bool showInBuildOutput)
		{
			AddWarningToList(null, message, file, line, showInBuildOutput);
		}

		public void AddErrorToList(string message, string file, int line, bool showInBuildOutput)
		{
			AddErrorToList(null, message, file, line, showInBuildOutput);
		}

		public void AddInfoToList(string category, string message, string file, int line)
		{
			AddInfoToList(category, message, file, line, false);
		}

		public void AddWarningToList(string category, string message, string file, int line)
		{
			AddWarningToList(category, message, file, line, false);
		}

		public void AddErrorToList(string category, string message, string file, int line)
		{
			AddErrorToList(category, message, file, line, false);
		}

		public void AddInfoToList(string category, string message, string file, int line, bool showInBuildOutput)
		{
			AddInfoToList(category, message, file, line, (showInBuildOutput) ? message : string.Empty);
		}

		public void AddWarningToList(string category, string message, string file, int line, bool showInBuildOutput)
		{
			AddWarningToList(category, message, file, line, (showInBuildOutput) ? message : string.Empty);
		}

		public void AddErrorToList(string category, string message, string file, int line, bool showInBuildOutput)
		{
			AddErrorToList(category, message, file, line, (showInBuildOutput) ? message : string.Empty);
		}

		public void AddInfoToList(string category, string message, string file, int line, string buildOutputMessage)
		{
			Invoke(delegate()
			{
				trace.TraceEvent(System.Diagnostics.TraceEventType.Information, category, message, file, line, buildOutputMessage);
			});
		}

		public void AddWarningToList(string category, string message, string file, int line, string buildOutputMessage)
		{
			Invoke(delegate()
			{
				trace.TraceEvent(System.Diagnostics.TraceEventType.Warning, category, message, file, line, buildOutputMessage);
			});
		}

		public void AddErrorToList(string category, string message, string file, int line, string buildOutputMessage)
		{
			Invoke(delegate()
			{
				itemErrorCount++;		// field is updated on UI thread since it's not volatile

				trace.TraceEvent(System.Diagnostics.TraceEventType.Error, category, message, file, line, buildOutputMessage);
			});
		}

		public void FlushItemsToList()
		{
			Invoke(delegate()
			{
				trace.FlushEvents();
			});
		}
		#endregion
		#endregion

		#region Events
		private readonly object CanceledChangedEventLock = new object();
		private EventHandler CanceledChangedEvent;

		/// <summary>
		/// Event raised after the <see cref="Canceled" /> property value has changed.
		/// </summary>
		public event EventHandler CanceledChanged
		{
			add
			{
				lock (CanceledChangedEventLock)
				{
					CanceledChangedEvent += value;
				}
			}
			remove
			{
				lock (CanceledChangedEventLock)
				{
					CanceledChangedEvent -= value;
				}
			}
		}

		/// <summary>
		/// Raises the <see cref="CanceledChanged" /> event.
		/// </summary>
		/// <param name="e"><see cref="EventArgs" /> object that provides the arguments for the event.</param>
		private void OnCanceledChanged(EventArgs e)
		{
			EventHandler handler = null;

			lock (CanceledChangedEventLock)
			{
				handler = CanceledChangedEvent;

				if (handler == null)
					return;
			}

			handler(this, e);
		}
		#endregion
	}
}
